package model;

import io.AudioPlay;
import io.Load;
import io.Save;

import org.bushe.swing.event.EventBus;
import org.bushe.swing.event.annotation.*;

import constants.Constants;
import constants.Events;
import model.playingfield.PlayingField;

/**
 * @author christopher
 * This the Model Class. It acts as a central control module for
 * the Model part of our design, and hence handles interaction with
 * all underlying subsystems on this part of the abstraction.
 * 
 * The Model also contains core logic related to the execution of the game.
 * Most importantly
 * It handles all interactions with the View.
 */

public class Model implements IModel {

	/**
	 * The Die, which will be used to determine the behavior of each players turn
	 */
	private Die die = new Die();

	/**
	 * The list of players currently in the game, indexed from 1 to 4
	 */
	private Player[] players;;

	/**
	 * The High-Score 
	 */

	/**
	 * The Rules for this session of the game. The Rules will determine how the
	 * game will respond to core events during the game.
	 */
	private IRules rules;

	/**
	 * The State for this session of the game. The State will store: 
	 * 	 - which players are in the game
	 *   - various state variables regarding the game.
	 */
	private IState state = new State();

	/**
	 * The Playingfield for this session of the game. The PlayingField stores 
	 * the location of pieces, and also provide an interface for moving them 
	 * around. 
	 */
	private PlayingField playingfield;

	/**
	 * The Statistics for the game in progress.
	 */
	private Statistics statistics;

	/**
	 * The audio in the game
	 */
	private AudioPlay audio = new AudioPlay();

	/**
	 * The timer in the game 
	 */
	private TurnTimer timer;

	/**
	 * The following are state variables, used in order to determine what phase
	 * of a players turn the Model currently is in, and data related to it:
	 *   - playerSelected : a player is currently selected
	 *   - dieRolled      : the die has been rolled
	 *   - spotSelected   : a piece has been selected
	 *   - moveMade 	  : the move has been made
	 *   
	 *   - currentPlayer  : the Player which the current turn belongs to
	 */	 

	public Model() {
		AnnotationProcessor.process(this);
	}

	@Override
	public void setState(IState state) {

		this.state = state;
	}
	@Override
	public IState getState() {

		return state;
	}

	/**
	 * The following functions are eventhandlers, and specify
	 * how the program should react to various events coming over
	 * the EventBus.
	 * 
	 * This is a crucial stage of functionality: the program should react
	 * to events in accordance with the values of it's state variables - it
	 * should for example not accept a new dicethrow if one has already been 
	 * done. 
	 */

	/**
	 * This is the first core event, and defines what happens when a DICETHROW
	 * event is broadcasted on the bus. The Model should simply catch the event,
	 * and consult the Rules class about what it is supposed to do.
	 */

	@EventTopicSubscriber(topic="DICETHROW")
	public void dicethrowEvent(String topic, Integer value) {
		System.out.println("Model received event " + topic);

		value = die.throwDie();


		/*
		 * Is there no game in progress? In that case, throw an error and 
		 * return.
		 */
		if(!state.isGameInProgress()) {
			EventBus.publish(Events.ERROR.toString(), "There is no game in progress!");
			return;
		}


		/*
		 * Calculate a new die value, and fire an event to make View simulate the way
		 * we acquired it for the user.
		 */
		EventBus.publish(Events.DICETHROWUPDATE.toString(), value);
		audio.playSound(Constants.Sounds.diethrow);
		/*
		 * Finally, execute the Rules logic, tell View to update, and 
		 * move forward.
		 */
		rules.dieRolled(value);		
		EventBus.publish(Events.UPDATE.toString(), this);
	}

	/**
	 * This is the second core event - after the dice is rolled, the Model will be
	 * idle until the player chooses to click somewhere on the playing field. The 
	 * Model will then catch an event related to this, and consult rules on what 
	 * to do with it.
	 * 
	 * After this, the Model will call the cleanUp() method from Rules in order to 
	 * perform last steps before passing the game on to the next player.
	 * @param topic
	 * @param data
	 */
	@EventTopicSubscriber(topic="TIME")
	public void onTimerTick(String topic, Integer turnTimeLeft){
		if(turnTimeLeft == 0){
			System.out.println("Another print");
			rules.onTimeOut();
			this.newTurn();
			EventBus.publish(Events.UPDATE.toString(), this);
		}
	}

	@EventTopicSubscriber(topic="END_GAME")
	public void onEndGame(String topic, Object unused){
		if(state.isGameInProgress()){
			state.resetStateVars();
			timer.stop();
		}
	}

	@EventTopicSubscriber(topic="PLAYERNAME")
	public void onPlayerNameChange(String topic, String[] players){
		for(int i = 0; i < 4;){
			if(players[i].compareTo("Default")==0){
				i++;
			}
			else{
				state.getPlayer(i).setPlayerName(players[i]);
				i++;
			}
		}
		EventBus.publish(Events.UPDATE.toString(), this);
	}

	@EventTopicSubscriber(topic="MUSIC")
	public void onMusic(String topic, Boolean onOFF){
		System.out.println("From model: Music is ON ");
		audio.setEnabled(onOFF);
	}

	@EventTopicSubscriber(topic="MOVE")
	public void selectSpotEvent(String topic, Integer location) {
		System.out.println("Model received event " + topic);

		/*
		 * Is there no game in progress? In that case, throw an error and 
		 * return.
		 */
		if(!state.isGameInProgress()) {
			EventBus.publish(Events.ERROR.toString(), "There is no game in progress!");
			return;
		}

		rules.movePiece(location, getState().getCurrentDieValue());
		//rules.cleanUp();

		if(state.getBattle() == null)
			EventBus.publish(Events.UPDATE.toString(), this);


	}

	/**
	 * This 
	 * @param topic
	 * @param data
	 */
	@EventTopicSubscriber(topic="SELECT_PLAYER")
	public void selectPlayerEvent(String topic, Object data) {
		System.out.println("Model received event " + topic);

	}

	@Override
	public void setRules(IRules rules) {
		// TODO Auto-generated method stub

	}

	/**
	 * This handles the NEWGAME event, which essentially causes the Model to wipe its 
	 * slate and start a completely new game.
	 * 
	 * This event will normally be thrown when the user clicks the "Start New Game" option from
	 * the menu. The user will be prompted by the system to enter some basic info, and this data
	 * will then be sent on to the Model, which will use it in order to initialize an environment
	 * for playing a new game.
	 */
	@EventTopicSubscriber(topic="NEWGAME")
	public void newGameEvent(String topic, Player[] players) {
		System.out.println("Model received event " + topic);
		newGame(players);
	}
	@EventTopicSubscriber(topic="BATTLE_ANS")
	public void onBattleAns(String topic, String answer) {
		System.out.println("Model received event " + topic);

		boolean correct = false;
		if(state.getBattle() != null){

			try{

				int theAnswer = Integer.parseInt(answer);
				System.out.println("ANSER: "+theAnswer+ ", Correct: "+state.getBattle().getQuestion().getAnswer());
				if(theAnswer == (int)(state.getBattle().getQuestion().getAnswer())){
					correct = true;
				}
			}catch(NumberFormatException e){
				correct = false;
			}
			if(!correct)
				EventBus.publish(Events.ERROR.toString(), "Oh no! Your answer was incorrect");
		}

		rules.resolveBattle(correct);
		state.setBattle(null);
		EventBus.publish(Events.UPDATE.toString(),this);
	}

	/**
	 * 
	 * @param topic
	 * @param fp the absolute filepath
	 */
	@EventTopicSubscriber(topic="SAVE_GAME")
	public void saveGameEvent(String topic, String fp) {
		System.out.println("Model received event " + topic + " " + fp);
		Save save = new Save(state, fp+Constants.IO.file_ext);
		save.writeToFile();
	}
	/**
	 * 
	 * @param topic
	 * @param fp the absolute filepath
	 */
	@EventTopicSubscriber(topic="LOAD_GAME")
	public void loadGameEvent(String topic, String fp) {
		System.out.println("Model received event " + topic + " " + fp);
		if(!state.isGameInProgress()){
			try {
				state = (new Load()).readFromFile(fp);
			} catch (ClassNotFoundException e) {
				e.printStackTrace();
			}
			Player[] p = new Player[4];
			for(int i = 0; i < state.getNumOfPlayers(); i++)
				p[i] = state.getPlayer(i);

			this.players = p;
			this.playingfield = state.getPlayingField();
			//this.statistics = state.getStatistics(); not implemented
			this.rules = new Rules(this);

			timer = new TurnTimer();
			timer.start();

			EventBus.publish(Events.UPDATE.toString(), this);

		}else
			EventBus.publish(Events.ERROR.toString(), "Cannot load while the game is in progress!");
	}

	/**
	 * Start a new turn. This involves performing several steps:
	 *   - Update statistics for the current turn	
	 *   - Reset state variables that were changed by the current turn
	 *   - Select a new player
	 */
	@Override
	public void newTurn() {
		System.out.println("NewTurn() called");
		/*
		 * Update the statistics for the current turn - that is, increase
		 * the currents players turnCounter, and add the time he or she spent
		 * on this turn, to this players total.
		 */

		/*
		 * Reset the various state variables, since we are about to
		 * embark on a new turn.
		 */

		state.setDieRolled(false);
		state.setSpotSelected(false);
		state.setMoveMade(false);

		/*
		 * Select a new current player. Make sure that we do not select a 
		 * player out of bounds, and make sure that the player being selected
		 * is still active in the game.
		 */

		Integer nextPlayer = state.getCurrentPlayer() + 1;
		for(int i = 0; i < 4; i++) {

			if(nextPlayer > 3)
				nextPlayer = 0;

			if(players[nextPlayer] != null) {

				state.setCurrentPlayer(nextPlayer);
				break;
			}
			nextPlayer++;
		}
		timer.restart();
		audio.playSound(Constants.Sounds.newTurn);
	}

	/**
	 * Starts a new game, complete with a new set of players, as well as a brand new State.
	 * When all the initialization is finished, fire an UPDATE event in order to let the View
	 * know it is time to redraw() itself to show the new data to the user.
	 */
	private void newGame(Player[] players) {
		System.out.println("newGame() called");
		if(state.isGameInProgress())
			onEndGame(null, null);
		this.players = players;
		this.playingfield = new PlayingField(players);		
		this.statistics = new Statistics(players);
		this.rules = new Rules(this);

		state = new State(players, playingfield, statistics);
		state.setGameInProgress(true);
		state.setCurrentPlayer(0);		

		timer = new TurnTimer();
		timer.start();
		audio.playSound(Constants.Sounds.newGame);
		EventBus.publish(Events.UPDATE.toString(), this);

	}

	@Override
	public Statistics getStatistics() {
		return statistics;
	}

	@Override
	public PlayingField getPlayingField() {
		if(playingfield == null)
			System.out.println("PLAYINGFIELD IS NULL");
		return playingfield;
	}

	public AudioPlay getAudio(){
		return audio;
	}
}

